1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   //     http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.services;
14  
15  import org.apache.tapestry5.internal.parser.*;
16  import org.apache.tapestry5.internal.test.InternalBaseTestCase;
17  import org.apache.tapestry5.ioc.Locatable;
18  import org.apache.tapestry5.ioc.Location;
19  import org.apache.tapestry5.ioc.Resource;
20  import org.apache.tapestry5.ioc.internal.util.AbstractResource;
21  import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
22  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
23  import org.apache.tapestry5.ioc.internal.util.TapestryException;
24  import org.apache.tapestry5.test.TapestryRunnerConstants;
25  import org.testng.annotations.DataProvider;
26  import org.testng.annotations.Test;
27  
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  
33  import static java.lang.String.format;
34  
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.net.URL;
38  
39  /**
40   * This is used to test the template parser ... and in some cases, the underlying behavior of the SAX APIs.
41   */
42  public class TemplateParserImplTest extends InternalBaseTestCase
43  {
44  
45      private TemplateParser getParser()
46      {
47          return this.getService(TemplateParser.class);
48      }
49  
50      private synchronized ComponentTemplate parse(String file)
51      {
52          Resource resource = getResource(file);
53  
54          return getParser().parseTemplate(resource);
55      }
56  
57      private synchronized List<TemplateToken> tokens(String file)
58      {
59          return parse(file).getTokens();
60      }
61  
62      private Resource getResource(String file)
63      {
64          String packageName = getClass().getPackage().getName();
65  
66          String path = packageName.replace('.', '/') + "/" + file;
67  
68          ClassLoader loader = getClass().getClassLoader();
69  
70          return new ClasspathResource(loader, path);
71      }
72  
73      @SuppressWarnings("unchecked")
74      private <T extends TemplateToken> T get(List l, int index)
75      {
76          Object raw = l.get(index);
77  
78          return (T) raw;
79      }
80  
81      private void checkType(List<TemplateToken> l, int index, TokenType expected)
82      {
83          assertEquals(l.get(index).getTokenType(), expected);
84      }
85  
86      private void checkLine(Locatable l, int expectedLineNumber)
87      {
88          assertEquals(l.getLocation().getLine(), expectedLineNumber);
89      }
90  
91      @Test
92      public void just_HTML()
93      {
94          Resource resource = getResource("justHTML.tml");
95  
96          ComponentTemplate template = getParser().parseTemplate(resource);
97  
98          assertSame(template.getResource(), resource);
99  
100         assertFalse(template.usesStrictMixinParameters());
101 
102         List<TemplateToken> tokens = template.getTokens();
103 
104         // They add up quick ...
105 
106         assertEquals(tokens.size(), 20);
107 
108         StartElementToken t0 = get(tokens, 0);
109 
110         // Spot check a few things ...
111 
112         assertEquals(t0.name, "html");
113         assertEquals(t0.namespaceURI, "");
114         checkLine(t0, 1);
115 
116         TextToken t1 = get(tokens, 1);
117         // Concerned this may not work cross platform.
118         assertEquals(t1.text, "\n    ");
119 
120         StartElementToken t2 = get(tokens, 2);
121         assertEquals(t2.name, "head");
122         checkLine(t2, 2);
123 
124         TextToken t5 = get(tokens, 5);
125         assertEquals(t5.text, "title");
126         checkLine(t5, 3);
127 
128         get(tokens, 6);
129 
130         StartElementToken t12 = get(tokens, 12);
131         assertEquals(t12.name, "p");
132 
133         AttributeToken t13 = get(tokens, 13);
134         assertEquals(t13.name, "class");
135         assertEquals(t13.value, "important");
136         assertEquals(t13.namespaceURI, "");
137 
138         TextToken t14 = get(tokens, 14);
139         // Simplify the text, converting consecutive whitespace to just a single space.
140         assertEquals(t14.text.replaceAll("\\s+", " ").trim(), "Tapestry rocks! Line 2");
141 
142         // Line number is the *start* line of the whole text block.
143         checkLine(t14, 6);
144     }
145 
146     @Test
147     public void namespaced_element()
148     {
149         Resource resource = getResource("namespaced_element.tml");
150 
151         ComponentTemplate template = getParser().parseTemplate(resource);
152 
153         assertSame(template.getResource(), resource);
154 
155         List<TemplateToken> tokens = template.getTokens();
156 
157         // They add up quick ...
158 
159         assertEquals(tokens.size(), 8);
160 
161         StartElementToken t0 = get(tokens, 0);
162 
163         String expectedURI = "http://foo.com";
164 
165         assertEquals(t0.namespaceURI, expectedURI);
166         assertEquals(t0.name, "bar");
167 
168         DefineNamespacePrefixToken t1 = get(tokens, 1);
169 
170         assertEquals(t1.namespacePrefix, "foo");
171         assertEquals(t1.namespaceURI, expectedURI);
172 
173         AttributeToken t2 = get(tokens, 2);
174 
175         assertEquals(t2.name, "biff");
176         assertEquals(t2.value, "baz");
177         assertEquals(t2.namespaceURI, expectedURI);
178 
179         StartElementToken t4 = get(tokens, 4);
180 
181         assertEquals(t4.namespaceURI, "");
182         assertEquals(t4.name, "gnip");
183 
184         // The rest are close tokens
185     }
186 
187     @Test
188     public void container_element()
189     {
190         List<TemplateToken> tokens = tokens("container_element.tml");
191 
192         assertEquals(tokens.size(), 4);
193 
194         TextToken t0 = get(tokens, 0);
195 
196         assertEquals(t0.text.trim(), "A bit of text.");
197 
198         StartElementToken t1 = get(tokens, 1);
199 
200         assertEquals(t1.name, "foo");
201 
202         EndElementToken t2 = get(tokens, 2);
203 
204         assertNotNull(t2); // Keep compiler happy
205 
206         TextToken t3 = get(tokens, 3);
207 
208         assertEquals(t3.text.trim(), "Some more text.");
209     }
210 
211     @Test
212     public void xml_entity()
213     {
214         List<TemplateToken> tokens = tokens("xmlEntity.tml");
215 
216         assertEquals(tokens.size(), 3);
217 
218         TextToken t = get(tokens, 1);
219 
220         // This is OK because the org.apache.tapestry5.dom.Text will convert the characters back into
221         // XML entities.
222 
223         assertEquals(t.text.trim(), "lt:< gt:> amp:&");
224     }
225 
226     @Test
227     public void html_entity()
228     {
229         String expectedURI = "http://www.w3.org/1999/xhtml";
230 
231         List<TemplateToken> tokens = tokens("html_entity.tml");
232 
233         assertEquals(tokens.size(), 5);
234 
235         DTDToken t0 = get(tokens, 0);
236 
237         assertEquals(t0.name, "html");
238         assertEquals(t0.publicId, "-//W3C//DTD XHTML 1.0 Transitional//EN");
239         assertEquals(t0.systemId, "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd");
240 
241         StartElementToken t1 = get(tokens, 1);
242 
243         assertEquals(t1.namespaceURI, expectedURI);
244         assertEquals(t1.name, "html");
245 
246         DefineNamespacePrefixToken t2 = get(tokens, 2);
247 
248         assertEquals(t2.namespaceURI, expectedURI);
249         assertEquals(t2.namespacePrefix, "");
250 
251 
252         TextToken t = get(tokens, 3);
253 
254         // HTML entities are parsed into values that will ultimately
255         // be output as numeric entities. This is less than ideal; would like
256         // to find a way to keep the entities in their original form (possibly
257         // involving a new type of token), but SAX seems to be fighting me on this.
258         // You have to have a DOCTYPE just to parse a template that uses
259         // an HTML entity.
260 
261         assertEquals(t.text.trim(), "nbsp:[\u00a0]");
262     }
263 
264     @Test
265     public void cdata()
266     {
267         List<TemplateToken> tokens = tokens("cdata.tml");
268 
269         // Whitespace text tokens around the CDATA
270 
271         assertEquals(tokens.size(), 5);
272 
273         CDATAToken t = get(tokens, 2);
274 
275         assertEquals(t.content, "CDATA: &lt;foo&gt; &amp; &lt;bar&gt; and <baz>");
276         checkLine(t, 2);
277     }
278 
279     @Test
280     public void comment()
281     {
282         List<TemplateToken> tokens = tokens("comment.tml");
283 
284         // Again, whitespace before and after the comment adds some tokens
285 
286         assertEquals(tokens.size(), 3);
287 
288         CommentToken token1 = get(tokens, 1);
289 
290         assertEquals(token1.comment, " Single line comment ");
291     }
292 
293     @Test
294     public void multiline_comment()
295     {
296         List<TemplateToken> tokens = tokens("multilineComment.tml");
297 
298         // Again, whitespace before and after the comment adds some tokens
299 
300         assertEquals(tokens.size(), 5);
301 
302         CommentToken t = get(tokens, 2);
303 
304         String comment = t.comment.trim().replaceAll("\\s+", " ");
305 
306         assertEquals(comment, "Line one Line two Line three");
307     }
308 
309     @Test
310     public void component()
311     {
312         List<TemplateToken> tokens = tokens("component.tml");
313 
314         assertEquals(tokens.size(), 6);
315 
316         StartComponentToken t = get(tokens, 2);
317         assertEquals(t.getId(), "fred");
318         assertEquals(t.getComponentType(), "somecomponent");
319         assertNull(t.getMixins());
320         checkLine(t, 2);
321 
322         get(tokens, 3);
323     }
324 
325     @Test
326     public void component_with_body()
327     {
328         List<TemplateToken> tokens = tokens("componentWithBody.tml");
329 
330         assertEquals(tokens.size(), 7);
331 
332         get(tokens, 2);
333 
334         TextToken t = get(tokens, 3);
335 
336         assertEquals(t.text.trim(), "fred's body");
337 
338         get(tokens, 4);
339     }
340 
341     /**
342      * @since 5.1.0.1
343      */
344     @Test
345     public void comment_element_ignored()
346     {
347         List<TemplateToken> tokens = tokens("comment_element_ignored.tml");
348 
349         assertEquals(tokens.size(), 8);
350 
351         get(tokens, 2);
352 
353         TextToken t = get(tokens, 3);
354 
355         assertEquals(t.text.trim(), "fred's body");
356 
357         EndElementToken end5 = get(tokens, 5);
358         EndElementToken end7 = get(tokens, 7);
359     }
360 
361     @Test
362     public void root_element_is_component()
363     {
364         List<TemplateToken> tokens = tokens("root_element_is_component.tml");
365 
366         assertEquals(tokens.size(), 3);
367 
368         StartComponentToken start = get(tokens, 0);
369 
370         assertEquals(start.getId(), "fred");
371         assertEquals(start.getComponentType(), "Fred");
372         assertNull(start.getElementName());
373 
374         AttributeToken attr = get(tokens, 1);
375 
376         assertEquals(attr.name, "param");
377         assertEquals(attr.value, "value");
378 
379         assertTrue(EndElementToken.class.isInstance(tokens.get(2)));
380     }
381 
382     @Test
383     public void instrumented_element()
384     {
385         ComponentTemplate template = parse("instrumented_element.tml");
386         List<TemplateToken> tokens = template.getTokens();
387 
388         assertEquals(tokens.size(), 3);
389 
390         StartComponentToken start = get(tokens, 0);
391 
392         assertEquals(start.getId(), "fred");
393         assertEquals(start.getComponentType(), "Fred");
394         assertEquals(start.getElementName(), "html");
395 
396         AttributeToken attr = get(tokens, 1);
397 
398         assertEquals(attr.name, "param");
399         assertEquals(attr.value, "value");
400 
401         assertTrue(EndElementToken.class.isInstance(tokens.get(2)));
402 
403         assertEquals(template.getComponentIds().keySet(), Arrays.asList("fred"));
404     }
405 
406     @Test
407     public void body_element()
408     {
409         List<TemplateToken> tokens = tokens("body_element.tml");
410 
411         // start(html), text, body, text, end(html)
412         assertEquals(tokens.size(), 5);
413 
414         // javac bug requires use of isInstance() instead of instanceof
415         // https://bugs.eclipse.org/bugs/show_bug.cgi?id=113218
416         assertTrue(BodyToken.class.isInstance(get(tokens, 2)));
417     }
418 
419     @Test
420     public void component_with_parameters()
421     {
422         List<TemplateToken> tokens = tokens("componentWithParameters.tml");
423 
424         assertEquals(tokens.size(), 9);
425 
426         TemplateToken templateToken = get(tokens, 2);
427         Location l = templateToken.getLocation();
428 
429         AttributeToken t1 = get(tokens, 3);
430 
431         // TODO: Not sure what order the attributes appear in. Order in the XML? Sorted
432         // alphabetically? Random 'cause they're hashed?
433 
434         assertEquals(t1.name, "cherry");
435         assertEquals(t1.value, "bomb");
436         assertSame(t1.getLocation(), l);
437 
438         AttributeToken t2 = get(tokens, 4);
439         assertEquals(t2.name, "align");
440         assertEquals(t2.value, "right");
441         assertSame(t2.getLocation(), l);
442 
443         TextToken t3 = get(tokens, 5);
444 
445         assertEquals(t3.text.trim(), "fred's body");
446 
447         get(tokens, 6);
448     }
449 
450     @Test
451     public void component_with_mixins()
452     {
453         List<TemplateToken> tokens = tokens("component_with_mixins.tml");
454 
455         assertEquals(tokens.size(), 4);
456 
457         StartComponentToken token1 = get(tokens, 1);
458 
459         assertEquals(token1.getId(), "fred");
460         assertEquals(token1.getComponentType(), "comp");
461         assertEquals(token1.getMixins(), "Barney");
462     }
463 
464     @Test
465     public void empty_string_mixins_is_null()
466     {
467         List<TemplateToken> tokens = tokens("empty_string_mixins_is_null.tml");
468 
469         assertEquals(tokens.size(), 6);
470 
471         StartComponentToken t = get(tokens, 2);
472 
473         assertEquals(t.getId(), "fred");
474         // We also check that empty string type is null ..
475         assertNull(t.getComponentType());
476         assertNull(t.getMixins());
477     }
478 
479     @Test
480     public void component_ids()
481     {
482         ComponentTemplate template = parse("component_ids.tml");
483 
484         Map<String, Location> map = template.getComponentIds();
485 
486         assertEquals(map.keySet(), CollectionFactory.newSet(Arrays.asList("bomb", "border", "zebra")));
487     }
488 
489     @Test
490     public void expansions_in_normal_text()
491     {
492         List<TemplateToken> tokens = tokens("expansions_in_normal_text.tml");
493 
494         assertEquals(tokens.size(), 7);
495 
496         TextToken t1 = get(tokens, 1);
497 
498         assertEquals(t1.text.trim(), "Expansion #1[");
499 
500         ExpansionToken t2 = get(tokens, 2);
501         assertEquals(t2.getExpression(), "expansion1");
502 
503         TextToken t3 = get(tokens, 3);
504         assertEquals(t3.text.replaceAll("\\s+", " "), "] Expansion #2[");
505 
506         ExpansionToken t4 = get(tokens, 4);
507         assertEquals(t4.getExpression(), "expansion2");
508 
509         TextToken t5 = get(tokens, 5);
510         assertEquals(t5.text.trim(), "]");
511     }
512 
513     @Test
514     public void expansions_must_be_on_one_line()
515     {
516         List<TemplateToken> tokens = tokens("expansions_must_be_on_one_line.tml");
517 
518         assertEquals(tokens.size(), 3);
519 
520         TextToken t1 = get(tokens, 1);
521 
522         assertEquals(t1.text.replaceAll("\\s+", " "), " ${expansions must be on a single line} ");
523     }
524 
525     @Test
526     public void multiple_expansions_on_one_line()
527     {
528         List<TemplateToken> tokens = tokens("multiple_expansions_on_one_line.tml");
529 
530         assertEquals(tokens.size(), 10);
531 
532         ExpansionToken token3 = get(tokens, 3);
533 
534         assertEquals(token3.getExpression(), "classLoader");
535 
536         TextToken token4 = get(tokens, 4);
537 
538         assertEquals(token4.text, " [");
539 
540         ExpansionToken token5 = get(tokens, 5);
541 
542         assertEquals(token5.getExpression(), "classLoader.class.name");
543 
544         TextToken token6 = get(tokens, 6);
545 
546         assertEquals(token6.text, "]");
547     }
548 
549     @Test
550     public void expansions_not_allowed_in_cdata()
551     {
552         List<TemplateToken> tokens = tokens("expansions_not_allowed_in_cdata.tml");
553 
554         assertEquals(tokens.size(), 5);
555 
556         CDATAToken t2 = get(tokens, 2);
557 
558         assertEquals(t2.content, "${not-an-expansion}");
559     }
560 
561     @Test
562     public void expansions_not_allowed_in_attributes()
563     {
564         List<TemplateToken> tokens = tokens("expansions_not_allowed_in_attributes.tml");
565 
566         assertEquals(tokens.size(), 3);
567 
568         AttributeToken token1 = get(tokens, 1);
569 
570         assertEquals(token1.name, "exp");
571         assertEquals(token1.value, "${not-an-expansion}");
572     }
573 
574     @Test
575     public void expansions_with_maps()
576     {
577         List<TemplateToken> tokens = tokens("expansions_with_maps.tml");
578 
579         assertEquals(tokens.size(), 11);
580 
581         //note that a single expansion on a line and two expansions on a line are tested individually elsewhere,
582         //so we group them together here just to cover all of the bases when using maps.
583         ExpansionToken expansion = get(tokens, 2);
584         assertEquals(expansion.getExpression(), "{}", "Empty map parsed incorrectly in an expansion");
585 
586         expansion = get(tokens, 4);
587         assertEquals(expansion.getExpression(), "{'a': 'b'}", "Non-empty map parsed incorrectly in an expansion");
588 
589         expansion = get(tokens, 6);
590         assertEquals(expansion.getExpression(), "{'one': 1}", "First expansion in a line with two expansions parsed incorrectly");
591 
592         expansion = get(tokens, 8);
593         assertEquals(expansion.getExpression(), "{'two': 2}", "Second expansion in a line with two expansions parsed incorrectly");
594     }
595 
596     @Test
597     public void expansion_whitespace_trimmed()
598     {
599         List<TemplateToken> tokens = tokens("expansions_with_whitespace.tml");
600 
601         assertEquals(tokens.size(), 9);
602 
603         ExpansionToken expansion = get(tokens, 2);
604         assertEquals(expansion.getExpression(), "message:messagekey1");
605 
606     }
607 
608     @Test
609     public void parameter_element()
610     {
611         List<TemplateToken> tokens = tokens("parameter_element.tml");
612 
613         ParameterToken token4 = get(tokens, 4);
614         assertEquals(token4.name, "fred");
615 
616         CommentToken token6 = get(tokens, 6);
617         assertEquals(token6.comment, " fred content ");
618 
619         TemplateToken token8 = get(tokens, 8);
620 
621         assertEquals(token8.getTokenType(), TokenType.END_ELEMENT);
622     }
623 
624     /**
625      * TAP5-112
626      */
627     @Test
628     public void parameter_namespace_element()
629     {
630         List<TemplateToken> tokens = tokens("parameter_namespace_element.tml");
631 
632         ParameterToken token4 = get(tokens, 4);
633         assertEquals(token4.name, "fred");
634 
635         CommentToken token6 = get(tokens, 6);
636         assertEquals(token6.comment, " fred content ");
637 
638         TemplateToken token8 = get(tokens, 8);
639 
640         assertEquals(token8.getTokenType(), TokenType.END_ELEMENT);
641     }
642 
643     @Test
644     public void complex_component_type()
645     {
646         List<TemplateToken> tokens = tokens("complex_component_type.tml");
647 
648         assertEquals(tokens.size(), 4);
649 
650         StartComponentToken token1 = get(tokens, 1);
651 
652         assertEquals(token1.getComponentType(), "subfolder/nifty");
653     }
654 
655     /**
656      * TAP5-66
657      */
658     @Test
659     public void component_inside_library_namespace()
660     {
661         List<TemplateToken> tokens = tokens("component_inside_library_namespace.tml");
662 
663         assertEquals(tokens.size(), 4);
664 
665         StartComponentToken token1 = get(tokens, 1);
666 
667         assertEquals(token1.getComponentType(), "subfolder/nifty");
668     }
669 
670     @Test
671     public void block_element()
672     {
673         List<TemplateToken> tokens = tokens("block_element.tml");
674 
675         BlockToken token1 = get(tokens, 1);
676         assertEquals(token1.getId(), "block0");
677 
678         CommentToken token2 = get(tokens, 2);
679         assertEquals(token2.comment, " block0 content ");
680 
681         BlockToken token4 = get(tokens, 4);
682         assertNull(token4.getId());
683 
684         CommentToken token5 = get(tokens, 5);
685         assertEquals(token5.comment, " anon block content ");
686     }
687 
688     @DataProvider
689     public Object[][] parse_failure_data()
690     {
691         return new Object[][]{
692 
693                 {"mixin_requires_id_or_type.tml",
694                         "You may not specify mixins for element <span> because it does not represent a component (which requires either an id attribute or a type attribute).",
695                         2},
696 
697                 {"unexpected_attribute_in_parameter_element.tml",
698                         "Element <parameter> does not support an attribute named 'grok'. The only allowed attribute name is 'name'.",
699                         4},
700 
701                 {"name_attribute_of_parameter_element_omitted.tml",
702                         "The name attribute of the <parameter> element must be specified.", 4},
703 
704                 {"name_attribute_of_parameter_element_blank.tml",
705                         "The name attribute of the <parameter> element must be specified.", 4},
706 
707                 {"unexpected_attribute_in_block_element.tml",
708                         "Element <block> does not support an attribute named 'name'. The only allowed attribute name is 'id'.",
709                         3},
710 
711                 {"parameter_namespace_with_attributes.tml",
712                         "A block parameter element does not allow any additional attributes. The element name defines the parameter name.", 4},
713 
714                 {"invalid_library_namespace_path.tml",
715                         "The path portion of library namespace URI 'tapestry-library:subfolder/' is not valid", 2},
716 
717                 {"content_within_body_element.tml", "Content inside a Tapestry body element is not allowed", 2},
718 
719                 {"nested_content_element.tml",
720                         "The <content> element may not be nested within another <content> element.", 3},
721 
722                 {"container_must_be_root.tml", "Element <container> is only valid as the root element of a template.",
723                         3},
724 
725                 {"extend_must_be_root.tml", "Element <extend> is only valid as the root element of a template.", 3},
726 
727                 {"replace_must_be_under_extend.tml",
728                         "The <replace> element may only appear directly within an extend element.", 3},
729 
730                 {"only_replace_within_extend.tml", "Child element of <extend> must be <replace>.", 2},
731 
732                 {"missing_id_in_replace_element.tml", "The <replace> element must have an id attribute.", 3},
733 
734                 {"extension_point_must_have_id.tml", "The <extension-point> element must have an id attribute.", 3},
735 
736                 {"misplaced_parameter.tml", "Block parameters are only allowed directly within component elements.",
737                         5},
738 
739                 {"parameter_namespace_element_deprecated.tml", "The <parameter> element has been deprecated in Tapestry 5.3 in favour of 'tapestry:parameter' namespace.", 4},
740 
741         };
742     }
743 
744     @Test(dataProvider = "parse_failure_data")
745     public void parse_failure(String fileName, String errorMessageSubstring, int expectedLine)
746     {
747         try
748         {
749             parse(fileName);
750             unreachable();
751         } catch (TapestryException ex)
752         {
753             if (!ex.getMessage().contains(errorMessageSubstring))
754             {
755                 throw new AssertionError(format("Message [%s] does not contain substring [%s].", ex.getMessage(),
756                         errorMessageSubstring));
757             }
758 
759             assertEquals(ex.getLocation().getLine(), expectedLine);
760         }
761     }
762 
763     @DataProvider
764     public Object[][] doctype_parsed_correctly_data()
765     {
766         return new Object[][]{{"xhtml1_strict_doctype.tml"}, {"xhtml1_transitional_doctype.tml"},
767                 {"xhtml1_frameset_doctype.tml"}};
768     }
769 
770     @Test(dataProvider = "doctype_parsed_correctly_data")
771     public void doctype_parsed_correctly(String fileName) throws Exception
772     {
773         List<TemplateToken> tokens = tokens(fileName);
774         assertEquals(tokens.size(), 12);
775         TextToken t = get(tokens, 9);
776         assertEquals(t.text.trim(), "<Test>");
777     }
778 
779     @DataProvider
780     public Object[][] doctype_token_added_correctly_data()
781     {
782         return new Object[][]{
783 
784                 {"xhtml1_strict_doctype.tml", "html", "-//W3C//DTD XHTML 1.0 Strict//EN",
785                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"},
786 
787                 {"xhtml1_transitional_doctype.tml", "html", "-//W3C//DTD XHTML 1.0 Transitional//EN",
788                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"},
789 
790                 {"xhtml1_frameset_doctype.tml", "html", "-//W3C//DTD XHTML 1.0 Frameset//EN",
791                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"},
792 
793                 {"html4_strict_doctype.tml", "HTML", "-//W3C//DTD HTML 4.01//EN",
794                         "http://www.w3.org/TR/html4/strict.dtd"},
795 
796                 {"html4_transitional_doctype.tml", "HTML", "-//W3C//DTD HTML 4.01 Transitional//EN",
797                         "http://www.w3.org/TR/html4/loose.dtd"},
798 
799                 {"html4_frameset_doctype.tml", "HTML", "-//W3C//DTD HTML 4.01 Frameset//EN",
800                         "http://www.w3.org/TR/html4/frameset.dtd"},
801 
802                 {"system_doctype.xml", "foo", null,
803                         "src/test/resources/org/apache/tapestry5/internal/services/simple.dtd"}};
804     }
805 
806     @Test(dataProvider = "doctype_token_added_correctly_data")
807     public void doctype_added_correctly(String fileName, String name, String publicId, String systemId) throws Exception
808     {
809         System.setProperty("user.dir", TapestryRunnerConstants.MODULE_BASE_DIR_PATH);
810 
811         List<TemplateToken> tokens = tokens(fileName);
812         DTDToken t0 = get(tokens, 0);
813         assertEquals(t0.name, name);
814         assertEquals(t0.publicId, publicId);
815         assertEquals(t0.systemId, systemId);
816     }
817 
818     @Test
819     public void invalid_component_id() throws Exception
820     {
821         try
822         {
823             parse("invalid_component_id.tml");
824             unreachable();
825         } catch (RuntimeException ex)
826         {
827             assertMessageContains(ex, "Component id 'not-valid' is not valid");
828         }
829     }
830 
831     @Test
832     public void invalid_block_id() throws Exception
833     {
834         try
835         {
836             parse("invalid_block_id.tml");
837             unreachable();
838         } catch (RuntimeException ex)
839         {
840             assertMessageContains(ex, "Block id 'not-valid' is not valid");
841         }
842     }
843 
844     /**
845      * Because of common code, this covers t:block and t:parameter.
846      */
847     @Test
848     public void space_preserved_in_block() throws Exception
849     {
850         List<TemplateToken> tokens = tokens("space_preserved_in_block.tml");
851 
852         TextToken token1 = get(tokens, 1);
853 
854         assertEquals(token1.text, "\n" + "        line in the middle\n" + "    ");
855     }
856 
857     /**
858      * t:container is a bit of a different code path than t:block/t:parameter
859      */
860     @Test
861     public void space_preserved_in_container() throws Exception
862     {
863         List<TemplateToken> tokens = tokens("space_preserved_in_container.tml");
864 
865         TextToken token0 = get(tokens, 0);
866         assertEquals(token0.text, "\n" + "    ");
867 
868         TextToken token2 = get(tokens, 2);
869         assertEquals(token2.text, "\n" + "        some text\n" + "    ");
870     }
871 
872     @Test
873     public void minimal_whitespace_maintained_inside_tags() throws Exception
874     {
875         List<TemplateToken> tokens = tokens("minimal_whitespace_maintained_inside_tags.tml");
876 
877         // A line feed or carriage return surrounded by other whitespace is reduced to
878         // just a line feed.
879 
880         TextToken token1 = get(tokens, 1);
881         assertEquals(token1.text, "\nWhitespace\n");
882 
883 
884         TextToken token5 = get(tokens, 5);
885         assertEquals(token5.text, "\nis maintained.\n");
886     }
887 
888     /**
889      * TAP5-563
890      */
891     @Test
892     public void content_element() throws Exception
893     {
894         List<TemplateToken> tokens = tokens("content_element.tml");
895 
896         assertEquals(tokens.size(), 5);
897 
898         StartComponentToken token0 = get(tokens, 0);
899         assertEquals(token0.getElementName(), "body");
900         assertEquals(token0.getComponentType(), "layout");
901 
902         StartElementToken token1 = get(tokens, 1);
903         assertEquals(token1.name, "p");
904 
905         TextToken token2 = get(tokens, 2);
906 
907         assertEquals(token2.text, "Page content");
908 
909         checkType(tokens, 3, TokenType.END_ELEMENT);
910         checkType(tokens, 4, TokenType.END_ELEMENT);
911     }
912 
913     @Test
914     public void overrides() throws Exception
915     {
916         ComponentTemplate template = parse("overrides.tml");
917 
918         assertTrue(template.isExtension());
919 
920         assertEquals(template.getTokens().size(), 0);
921 
922         List<TemplateToken> alpha = template.getExtensionPointTokens("alpha");
923 
924         assertEquals(alpha.size(), 1);
925 
926         TextToken alpha0 = get(alpha, 0);
927         assertEquals(alpha0.text, "beta");
928 
929         List<TemplateToken> gamma = template.getExtensionPointTokens("gamma");
930         assertEquals(gamma.size(), 3);
931 
932         StartElementToken gamma0 = get(gamma, 0);
933         assertEquals(gamma0.name, "p");
934 
935         TextToken gamma1 = get(gamma, 1);
936 
937         assertEquals(gamma1.text, "Hi!");
938 
939         checkType(gamma, 2, TokenType.END_ELEMENT);
940     }
941 
942     @Test
943     public void extension_point() throws Exception
944     {
945         ComponentTemplate template = parse("extension_point.tml");
946 
947         ExtensionPointToken expansion = get(template.getTokens(), 2);
948 
949         assertEquals(expansion.getExtensionPointId(), "title");
950 
951         List<TemplateToken> title = template.getExtensionPointTokens("title");
952 
953         assertEquals(title.size(), 3);
954 
955         StartElementToken title0 = get(title, 0);
956         assertEquals(title0.name, "h1");
957 
958         TextToken title1 = get(title, 1);
959         assertEquals(title1.text, "Default Title");
960 
961         checkType(title, 2, TokenType.END_ELEMENT);
962     }
963 
964     @Test
965     public void html5_with_entities() throws Exception
966     {
967         List<TemplateToken> tokens = tokens("html5_with_entities.tml");
968 
969         assertEquals(tokens.size(), 5);
970 
971         DTDToken token0 = get(tokens, 0);
972         assertEquals(token0.toString(), "DTD[name=html; publicId=null; systemId=null]");
973 
974         TextToken token3 = get(tokens, 3);
975 
976         assertEquals(token3.text, "\u00A92011\u00A0Apache");
977     }
978 
979     /**
980      * https://issues.apache.org/jira/browse/TAP5-1329
981      */
982     @Test
983     public void dupe_extension_point_id() throws Exception
984     {
985         try
986         {
987             tokens("dupe_extension_point_id.tml");
988             unreachable();
989         } catch (Exception ex)
990         {
991             assertMessageContains(ex, "Extension point 'batman' is already defined for this template.");
992         }
993     }
994 
995     @Test
996     public void html_entities_inside_template_without_doctype_are_allowed() throws Exception
997     {
998         List<TemplateToken> tokens = tokens("html_entities.tml");
999 
1000         assertEquals(tokens.size(), 3);
1001 
1002         StartElementToken token0 = get(tokens, 0);
1003 
1004         assertEquals(token0.name, "html");
1005 
1006         TextToken token1 = get(tokens, 1);
1007 
1008         assertEquals(token1.text, "\n[\u00A0]\n");
1009     }
1010 
1011     @Test
1012     public void utf8_template() throws Exception
1013     {
1014         List<TemplateToken> tokens = tokens("chinese_utf-8.tml");
1015 
1016         TextToken token7 = get(tokens, 7);
1017 
1018         assertEquals(token7.text.trim().substring(0, 3), "\u975E\u5e38\u7b80");
1019     }
1020 
1021     @Test
1022     public void block_can_nest_inside_extend() throws Exception
1023     {
1024         List<TemplateToken> tokens = tokens("block_can_nest_inside_extend.tml");
1025 
1026         BlockToken token = get(tokens, 0);
1027 
1028         assertEquals(token.getId(), "myBlock");
1029     }
1030 
1031     /**
1032      * See <a href="https://issues.apache.org/jira/browse/TAP5-1976">TAP5-1976</a>
1033      *
1034      * @since 5.3.5
1035      */
1036     @Test
1037     public void default_attributes_not_included()
1038     {
1039         List<TemplateToken> tokens = tokens("default_attributes_not_included.tml");
1040 
1041         assertEquals(tokens.size(), 2);
1042 
1043         // TAP5-1976 meant that a default attribute ("Attribute[shape=rect]") would be included.
1044 
1045         assertEquals(toString(tokens), "Start[a] End");
1046     }
1047 
1048     @Test
1049     public void t54_DTDs_are_strict_about_mixin_parameters() {
1050 
1051         assertTrue(parse("instrumented_element.tml").usesStrictMixinParameters());
1052     }
1053 
1054     private String toString(List<TemplateToken> tokens)
1055     {
1056         StringBuilder builder = new StringBuilder();
1057         String sep = "";
1058 
1059         for (TemplateToken token : tokens)
1060         {
1061             builder.append(sep).append(token.toString());
1062             sep = " ";
1063         }
1064 
1065         return builder.toString();
1066     }
1067 
1068     @Test
1069     public void text_from_content_not_dropped() {
1070         List<TemplateToken> tokens = tokens("TAP5-2109.tml");
1071 
1072         System.out.println(tokens);
1073 
1074         assertEquals(tokens.size(), 4);
1075 
1076         TextToken t0 = get(tokens, 0);
1077 
1078         assertEquals(t0.text.trim(), "BEGIN");
1079 
1080         StartComponentToken t1 = get(tokens, 1);
1081         assertEquals(t1.getComponentType(), "somecomponent");
1082 
1083         EndElementToken t2 = get(tokens, 2);
1084 
1085         TextToken t3 = get(tokens, 3);
1086 
1087         assertEquals(t3.text.trim(), "END");
1088     }
1089 
1090     @Test
1091     //TAP5-2516
1092     public void resource_that_throws_exception() throws Exception
1093     {
1094 
1095         Resource resource = new AbstractResource("throwfoo") {
1096 
1097           @Override
1098           public URL toURL() {
1099             return null;
1100           }
1101 
1102           @Override
1103           public boolean exists() {
1104             return true;
1105           }
1106 
1107           @Override
1108           protected Resource newResource(String path) {
1109             return null;
1110           }
1111 
1112           @Override
1113           public InputStream openStream() throws IOException {
1114             throw new IOException("foo");
1115           }
1116         };
1117 
1118 
1119         try
1120         {
1121             getParser().parseTemplate(resource);
1122             unreachable();
1123         } catch (RuntimeException ex)
1124         {
1125             if (ex.getCause() instanceof TapestryException && ex.getCause().getCause() instanceof IOException)
1126             {
1127                 assertMessageContains(ex, "foo");
1128             } else {
1129                 throw ex;
1130             }
1131         }
1132     }
1133 }